// 版权所有2011 Go作者。版权所有。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package zip

import (
	"bufio"
	"encoding/binary"
	"errors"
	"hash"
	"hash/crc32"
	"io"
	"strings"
	"unicode/utf8"
)

var (
	errLongName  = errors.New("zip: FileHeader.Name too long")
	errLongExtra = errors.New("zip: FileHeader.Extra too long")
)

// Writer实现了一个zip文件编写器。
type Writer struct {
	cw          *countWriter
	dir         []*header
	last        *fileWriter
	closed      bool
	compressors map[uint16]Compressor
	comment     string

	// testHookCloseSizeOffset如果使用关闭时中心目录偏移量的大小
	// 调用非nil。
	testHookCloseSizeOffset func(size, offset uint64)
}

type header struct {
	*FileHeader
	offset uint64
	raw    bool
}

// NewWriter返回一个新的Writer，将zip文件写入w。
func NewWriter(w io.Writer) *Writer {
	return &Writer{cw: &countWriter{w: bufio.NewWriter(w)}}
}

// SetOffset设置基础Writer中zip数据开头的偏移量。当zip数据被附加到
// 现有文件（例如二进制可执行文件）时，应该使用它。
// 必须在写入任何数据之前调用它。
func (w *Writer) SetOffset(n int64) {
	if w.cw.count != 0 {
		panic("zip: SetOffset called after data was written")
	}
	w.cw.count = n
}

// Flush将所有缓冲数据刷新到基础写入程序。
// 通常不需要调用Flush；打电话给Close就足够了。
func (w *Writer) Flush() error {
	return w.cw.w.(*bufio.Writer).Flush()
}

// SetComment设置中央目录注释字段的结尾。
// 只能在关闭前调用。
func (w *Writer) SetComment(comment string) error {
	if len(comment) > uint16max {
		return errors.New("zip: Writer.Comment too long")
	}
	w.comment = comment
	return nil
}

// Close通过写入中心目录来完成zip文件的写入。
// 它不会关闭基础writer。
func (w *Writer) Close() error {
	if w.last != nil && !w.last.closed {
		if err := w.last.close(); err != nil {
			return err
		}
		w.last = nil
	}
	if w.closed {
		return errors.New("zip: writer closed twice")
	}
	w.closed = true

	// 写入中心目录
	start := w.cw.count
	for _, h := range w.dir {
		var buf [directoryHeaderLen]byte
		b := writeBuf(buf[:])
		b.uint32(uint32(directoryHeaderSignature))
		b.uint16(h.CreatorVersion)
		b.uint16(h.ReaderVersion)
		b.uint16(h.Flags)
		b.uint16(h.Method)
		b.uint16(h.ModifiedTime)
		b.uint16(h.ModifiedDate)
		b.uint32(h.CRC32)
		if h.isZip64() || h.offset >= uint32max {
			// 文件需要zip64头。在
			// 32位大小字段（以及稍后的偏移量）中存储maxint，以表示应该使用
			// zip64额外头。
			b.uint32(uint32max) // 压缩大小
			b.uint32(uint32max) // 未压缩大小

			// 向额外
			var buf [28]byte // 2x uint16+3x uint64 
			eb := writeBuf(buf[:])
			eb.uint16(zip64ExtraID)
			eb.uint16(24) // size=3x uint64 
			eb.uint64(h.UncompressedSize64)
			eb.uint64(h.CompressedSize64)
			eb.uint64(h.offset)
			h.Extra = append(h.Extra, buf[:]...)
		} else {
			b.uint32(h.CompressedSize)
			b.uint32(h.UncompressedSize)
		}

		b.uint16(uint16(len(h.Name)))
		b.uint16(uint16(len(h.Extra)))
		b.uint16(uint16(len(h.Comment)))
		b = b[4:] // 跳过磁盘号开始和内部文件属性（2x uint16）
		b.uint32(h.ExternalAttrs)
		if h.offset > uint32max {
			b.uint32(uint32max)
		} else {
			b.uint32(uint32(h.offset))
		}
		if _, err := w.cw.Write(buf[:]); err != nil {
			return err
		}
		if _, err := io.WriteString(w.cw, h.Name); err != nil {
			return err
		}
		if _, err := w.cw.Write(h.Extra); err != nil {
			return err
		}
		if _, err := io.WriteString(w.cw, h.Comment); err != nil {
			return err
		}
	}
	end := w.cw.count

	records := uint64(len(w.dir))
	size := uint64(end - start)
	offset := uint64(start)

	if f := w.testHookCloseSizeOffset; f != nil {
		f(size, offset)
	}

	if records >= uint16max || size >= uint32max || offset >= uint32max {
		var buf [directory64EndLen + directory64LocLen]byte
		b := writeBuf(buf[:])

		// zip64中央目录记录的结尾
		b.uint32(directory64EndSignature)
		b.uint16(zipVersion45)           // 版本由
		b.uint16(zipVersion45)           // 版本需要提取
		b.uint32(0)                      // 此磁盘的数量
		b.uint32(0)                      // 中心目录开始的磁盘数量
		b.uint64(records)                // 此磁盘中心目录中的条目数
		b.uint64(records)                // 中心目录中的条目数目录
		b.uint64(size)                   // 中心目录的大小
		b.uint64(offset)                 // 中心目录的开始相对于起始磁盘的偏移量

		// zip64中心目录的结束定位器
		b.uint32(directory64LocSignature)
		b.uint32(0)           // 中心目录的开始相对于zip64结束的磁盘的偏移量
		b.uint64(uint64(end)) // 中心目录记录zip64结束的相对偏移量
		b.uint32(1)           // 磁盘总数

		if _, err := w.cw.Write(buf[:]); err != nil {
			return err
		}

		// 在常规结束记录中存储最大值，以表示
		// 应使用zip64值来代替
		records = uint16max
		size = uint32max
		offset = uint32max
	}

	// 写入结束记录
	var buf [directoryEndLen]byte
	b := writeBuf(buf[:])
	b.uint32(uint32(directoryEndSignature))
	b = b[4:]                        // 跳过磁盘号和第一个磁盘号（2x uint16）
	b.uint16(uint16(records))        // 此磁盘的条目数
	b.uint16(uint16(records))        // 条目数总计
	b.uint32(uint32(size))           // 目录的大小
	b.uint32(uint32(offset))         // 目录的开始
	b.uint16(uint16(len(w.comment))) // EOCD注释的字节大小
	if _, err := w.cw.Write(buf[:]); err != nil {
		return err
	}
	if _, err := io.WriteString(w.cw, w.comment); err != nil {
		return err
	}

	return w.cw.w.(*bufio.Writer).Flush()
}

// Create使用提供的名称将文件添加到zip文件中。
// 返回文件内容应该写入的写入程序。
// 文件内容将使用Deflate方法进行压缩。
// 名称必须是相对路径：它不能以驱动器
// 字母（例如C:）或前导斜杠开头，并且只允许正斜杠
// 。要创建目录而不是文件，请在名称后面添加一个
// 斜杠。
// 文件内容必须写入io。下一个
// 调用Create、CreateHeader或Close之前的Writer。
func (w *Writer) Create(name string) (io.Writer, error) {
	header := &FileHeader{
		Name:   name,
		Method: Deflate,
	}
	return w.CreateHeader(header)
}

// detectUTF8报告s是否为有效的UTF-8字符串，以及字符串
// 是否必须被视为UTF-8编码（即，与CP-437、ASCII、
// 或任何其他常见编码不兼容）。
func detectUTF8(s string) (valid, require bool) {
	for i := 0; i < len(s); {
		r, size := utf8.DecodeRuneInString(s[i:])
		i += size
		// ZIP正式使用CP-437，但许多读者使用系统的
		// 本地字符编码。大多数编码都与CP-437的大型
		// 子集兼容，后者本身类似于ASCII。
		// 
		// 禁止0x7e和0x5c，因为EUC-KR和Shift JIS将这些
		// 字符替换为本地化货币和下划线字符。
		if r < 0x20 || r > 0x7d || r == 0x5c {
			if !utf8.ValidRune(r) || (r == utf8.RuneError && size == 1) {
				return false, false
			}
			require = true
		}
	}
	return true, require
}

// prepare执行
// CreateHeader和CreateRaw开始时所需的记账操作。
func (w *Writer) prepare(fh *FileHeader) error {
	if w.last != nil && !w.last.closed {
		if err := w.last.close(); err != nil {
			return err
		}
	}
	if len(w.dir) > 0 && w.dir[len(w.dir)-1].FileHeader == fh {
		// 请参阅https:
		return errors.New("archive/zip: invalid duplicate FileHeader")
	}
	return nil
}

// CreateHeader使用提供的文件头
// 将文件添加到zip存档中，以获取文件元数据。Writer拥有fh的所有权，可能会对
// 及其字段进行变异。调用方在调用CreateHeader后不得修改fh。
// 
// 返回文件内容应该写入的写入程序。
// 文件内容必须写入io。下一个
// 调用Create、CreateHeader、CreateRaw或Close之前的Writer。
func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
	if err := w.prepare(fh); err != nil {
		return nil, err
	}

	// ZIP格式在字符编码方面有一个令人悲伤的状态。
	// 官方规定，除非设置了UTF-8 
	// 。然而，有几个问题：
	// 标志位，否则名称和注释字段应在CP-437（主要与ASCII兼容）中编码为
	// 
	// *许多ZIP阅读器仍然不支持UTF-8。
	// *如果清除UTF-8标志，几个读者只需将
	// 名称和注释字段解释为本地系统编码。
	// 
	// 为了避免在没有UTF-8支持的情况下中断读卡器，
	// 如果字符串与CP-437兼容，我们将避免设置UTF-8标志。
	// 但是，如果字符串需要多字节UTF-8编码，并且是有效的UTF-8字符串，那么我们设置UTF-8位。
	// 
	// 对于用户明确希望将编码
	// 指定为UTF-8的情况，他们需要自己设置标志位。
	utf8Valid1, utf8Require1 := detectUTF8(fh.Name)
	utf8Valid2, utf8Require2 := detectUTF8(fh.Comment)
	switch {
	case fh.NonUTF8:
		fh.Flags &^= 0x800
	case (utf8Require1 || utf8Require2) && (utf8Valid1 && utf8Valid2):
		fh.Flags |= 0x800
	}

	fh.CreatorVersion = fh.CreatorVersion&0xff00 | zipVersion20 // 保留兼容字节
	fh.ReaderVersion = zipVersion20

	// 如果设置了修改，则优先于MS-DOS时间戳字段。
	if !fh.Modified.IsZero() {
		// 与文件头相反。SetModTime方法，我们故意
		// 不转换为UTC，因为我们假设用户打算使用指定的时区编码
		// 日期。用户可能需要此控件
		// 因为许多传统的ZIP读取器根据本地时区解释时间戳。
		// 
		// 如果用户直接自己设置修改后的
		// 字段，则时区仅为非UTC。所有其他方法设置UTC。
		fh.ModifiedDate, fh.ModifiedTime = timeToMsDosTime(fh.Modified)

		// 使用“扩展时间戳”格式，因为这是Info ZIP使用的格式。
		// 几乎每个主要的ZIP实现都使用不同的格式，但至少大多数人似乎能够理解其他格式。
		// 
		// 如果修改时间是唯一被编码的时间戳，那么本地和中心头的格式恰好相同。
		var mbuf [9]byte // 2*SizeOf（uint16）+SizeOf（uint8）+SizeOf（uint32）
		mt := uint32(fh.Modified.Unix())
		eb := writeBuf(mbuf[:])
		eb.uint16(extTimeExtraID)
		eb.uint16(5)  // Size:SizeOf（uint8）+SizeOf（uint32）
		eb.uint8(1)   // Flags:ModTime 
		eb.uint32(mt) // ModTime 
		fh.Extra = append(fh.Extra, mbuf[:]...)
	}

	var (
		ow io.Writer
		fw *fileWriter
	)
	h := &header{
		FileHeader: fh,
		offset:     uint64(w.cw.count),
	}

	if strings.HasSuffix(fh.Name, "/") {
		// 设置要存储的压缩方法以确保数据长度真正为零，
		// 这是必要的，因为大多数压缩格式的长度都不是零
		// 即使在压缩空字符串时也是如此。
		fh.Method = Store
		fh.Flags &^= 0x8 // 我们不会编写数据描述符

		// 显式清除大小，因为它们对目录没有意义。
		fh.CompressedSize = 0
		fh.CompressedSize64 = 0
		fh.UncompressedSize = 0
		fh.UncompressedSize64 = 0

		ow = dirWriter{}
	} else {
		fh.Flags |= 0x8 // 我们将编写一个数据描述符

		fw = &fileWriter{
			zipw:      w.cw,
			compCount: &countWriter{w: w.cw},
			crc32:     crc32.NewIEEE(),
		}
		comp := w.compressor(fh.Method)
		if comp == nil {
			return nil, ErrAlgorithm
		}
		var err error
		fw.comp, err = comp(fw.compCount)
		if err != nil {
			return nil, err
		}
		fw.rawCount = &countWriter{w: fw.comp}
		fw.header = h
		ow = fw
	}
	w.dir = append(w.dir, h)
	if err := writeHeader(w.cw, h); err != nil {
		return nil, err
	}
	// 如果我们正在创建一个目录，fw为零。
	w.last = fw
	return ow, nil
}

func writeHeader(w io.Writer, h *header) error {
	const maxUint16 = 1<<16 - 1
	if len(h.Name) > maxUint16 {
		return errLongName
	}
	if len(h.Extra) > maxUint16 {
		return errLongExtra
	}

	var buf [fileHeaderLen]byte
	b := writeBuf(buf[:])
	b.uint32(uint32(fileHeaderSignature))
	b.uint16(h.ReaderVersion)
	b.uint16(h.Flags)
	b.uint16(h.Method)
	b.uint16(h.ModifiedTime)
	b.uint16(h.ModifiedDate)
	// 在原始模式（调用者进行压缩）下，值可以是写入此处的
	// 或基于头
	// 标志的尾部数据描述符。
	if h.raw && !h.hasDataDescriptor() {
		b.uint32(h.CRC32)
		b.uint32(uint32(min64(h.CompressedSize64, uint32max)))
		b.uint32(uint32(min64(h.UncompressedSize64, uint32max)))
	} else {
		// 当这个包处理压缩时，这些值总是写入到后面的数据描述符中。
		b.uint32(0) // crc32 
		b.uint32(0) // 压缩大小
		b.uint32(0) // 未压缩大小
	}
	b.uint16(uint16(len(h.Name)))
	b.uint16(uint16(len(h.Extra)))
	if _, err := w.Write(buf[:]); err != nil {
		return err
	}
	if _, err := io.WriteString(w, h.Name); err != nil {
		return err
	}
	_, err := w.Write(h.Extra)
	return err
}

func min64(x, y uint64) uint64 {
	if x < y {
		return x
	}
	return y
}

// CreateRaw使用提供的文件头将文件添加到zip存档中，并返回文件内容应写入的写入器。文件的
// 内容必须写入io。下一次调用Create之前的Writer，
// CreateHeader、CreateRaw或Close。
// 
// 与CreateHeader相比，传递给Writer的字节没有被压缩。
func (w *Writer) CreateRaw(fh *FileHeader) (io.Writer, error) {
	if err := w.prepare(fh); err != nil {
		return nil, err
	}

	fh.CompressedSize = uint32(min64(fh.CompressedSize64, uint32max))
	fh.UncompressedSize = uint32(min64(fh.UncompressedSize64, uint32max))

	h := &header{
		FileHeader: fh,
		offset:     uint64(w.cw.count),
		raw:        true,
	}
	w.dir = append(w.dir, h)
	if err := writeHeader(w.cw, h); err != nil {
		return nil, err
	}

	if strings.HasSuffix(fh.Name, "/") {
		w.last = nil
		return dirWriter{}, nil
	}

	fw := &fileWriter{
		header: h,
		zipw:   w.cw,
	}
	w.last = fw
	return fw, nil
}

// 复制将文件f（从读卡器获得）复制到w。它直接绕过解压缩、压缩和验证复制原始
// 表单。
func (w *Writer) Copy(f *File) error {
	r, err := f.OpenRaw()
	if err != nil {
		return err
	}
	fw, err := w.CreateRaw(&f.FileHeader)
	if err != nil {
		return err
	}
	_, err = io.Copy(fw, r)
	return err
}

// RegisterCompressor为特定的
// 方法ID注册或重写自定义压缩器。如果找不到给定方法的压缩器，编写器将默认在包级别查找压缩器。
func (w *Writer) RegisterCompressor(method uint16, comp Compressor) {
	if w.compressors == nil {
		w.compressors = make(map[uint16]Compressor)
	}
	w.compressors[method] = comp
}

func (w *Writer) compressor(method uint16) Compressor {
	comp := w.compressors[method]
	if comp == nil {
		comp = compressor(method)
	}
	return comp
}

type dirWriter struct{}

func (dirWriter) Write(b []byte) (int, error) {
	if len(b) == 0 {
		return 0, nil
	}
	return 0, errors.New("zip: write to directory")
}

type fileWriter struct {
	*header
	zipw      io.Writer
	rawCount  *countWriter
	comp      io.WriteCloser
	compCount *countWriter
	crc32     hash.Hash32
	closed    bool
}

func (w *fileWriter) Write(p []byte) (int, error) {
	if w.closed {
		return 0, errors.New("zip: write to closed file")
	}
	if w.raw {
		return w.zipw.Write(p)
	}
	w.crc32.Write(p)
	return w.rawCount.Write(p)
}

func (w *fileWriter) close() error {
	if w.closed {
		return errors.New("zip: file closed twice")
	}
	w.closed = true
	if w.raw {
		return w.writeDataDescriptor()
	}
	if err := w.comp.Close(); err != nil {
		return err
	}

	// 更新文件头
	fh := w.header.FileHeader
	fh.CRC32 = w.crc32.Sum32()
	fh.CompressedSize64 = uint64(w.compCount.count)
	fh.UncompressedSize64 = uint64(w.rawCount.count)

	if fh.isZip64() {
		fh.CompressedSize = uint32max
		fh.UncompressedSize = uint32max
		fh.ReaderVersion = zipVersion45 // 需要4.5-文件使用ZIP64格式扩展
	} else {
		fh.CompressedSize = uint32(fh.CompressedSize64)
		fh.UncompressedSize = uint32(fh.UncompressedSize64)
	}

	return w.writeDataDescriptor()
}

func (w *fileWriter) writeDataDescriptor() error {
	if !w.hasDataDescriptor() {
		return nil
	}
	// 写入数据描述符。这比人们想象的要复杂得多，参见zipfile中的注释。c:putextended（）和
	// http:
	// 这里的方法是，如果需要，在不使用
	// 的情况下写入8字节大小（无论如何都太迟了）。
	var buf []byte
	if w.isZip64() {
		buf = make([]byte, dataDescriptor64Len)
	} else {
		buf = make([]byte, dataDescriptorLen)
	}
	b := writeBuf(buf)
	b.uint32(dataDescriptorSignature) // 事实标准，由OS X 
	b.uint32(w.CRC32)
	if w.isZip64() {
		b.uint64(w.CompressedSize64)
		b.uint64(w.UncompressedSize64)
	} else {
		b.uint32(w.CompressedSize)
		b.uint32(w.UncompressedSize)
	}
	_, err := w.zipw.Write(buf)
	return err
}

type countWriter struct {
	w     io.Writer
	count int64
}

func (w *countWriter) Write(p []byte) (int, error) {
	n, err := w.w.Write(p)
	w.count += int64(n)
	return n, err
}

type nopCloser struct {
	io.Writer
}

func (w nopCloser) Close() error {
	return nil
}

type writeBuf []byte

func (b *writeBuf) uint8(v uint8) {
	(*b)[0] = v
	*b = (*b)[1:]
}

func (b *writeBuf) uint16(v uint16) {
	binary.LittleEndian.PutUint16(*b, v)
	*b = (*b)[2:]
}

func (b *writeBuf) uint32(v uint32) {
	binary.LittleEndian.PutUint32(*b, v)
	*b = (*b)[4:]
}

func (b *writeBuf) uint64(v uint64) {
	binary.LittleEndian.PutUint64(*b, v)
	*b = (*b)[8:]
}
